﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Collections.ObjectModel;
using Medianamik.Core.Querying;
using Medianamik.Core.Sugar;

namespace Medianamik.Core.Synchronization
{
    public class NodeSynchronizer : MedianamikSynchronizer<Record, Node>
    {
        public NodeSynchronizer() { }
        public NodeSynchronizer(SynchronizationOptions options)
            : base(options)
        {
        }

        protected override Node FindExisting(Record source)
        {
            if (String.IsNullOrEmpty(source.IdentifierPropertyName) ||
                String.IsNullOrEmpty(source.TemporaryId))
            {
                return base.FindExisting(source);
            }

            var result = TResultBag
                .Where(t => t.Status != EntityStates.Deleted &&
                    t.Status != EntityStates.DeletedBranch && t.NodeTypeID.Equals(source.NodeTypeId.Value))
                .FirstOrDefault(t => String.Compare(t.DefaultInstance
                    .Properties[source.IdentifierPropertyName]
                    .GetValue<string>(), source.TemporaryId, true) == 0);

            if (result == null)
            {
                result = GetTResult(source);

                if (result != null)
                {
                    source.Id = result.ID;
                    TResultBag.Add(result);
                }
            }

            return result;
        }

        protected override Node GetTResult(Record source)
        {
            Node returnNode = null;
            if (String.IsNullOrEmpty(source.IdentifierPropertyName) ||
                String.IsNullOrEmpty(source.TemporaryId))
            {
                if (source.Id.HasValue)
                    returnNode = NodeManager.GetNodeEvenIfDeleted(source.Id.Value);
            }
            else
            {
                if (Options.GetNodesFromDatabase)
                    returnNode = GetNodeFromDatabaseByImportId(source);
            }

            return returnNode;

        }

        protected virtual Node GetNodeFromDatabaseByImportId(Record source)
        {
            var nodes = new Collection<Node>();

            if (!String.IsNullOrEmpty(source.NodeTypeName) &&
                !String.IsNullOrEmpty(source.IdentifierPropertyName))
                nodes = NodeManager.GetNodesEvenIfDeletedWithTypeSpecified(new Param(source.NodeTypeName,
                    source.IdentifierPropertyName, source.TemporaryId), source.NodeTypeName);

            return nodes.FirstOrDefault();
        }

        //Important : Les business rules doivent avoir été validées avant d'arriver ici
        //Important : On ne fait plus de validation rendu ici

        /// <summary>
        /// Mettre le node existingItem à jour selon la source
        /// Important : Les business rules doivent avoir été validées avant d'arriver ici
        /// Important : On ne fait plus de validation rendu ici
        /// </summary>
        /// <param name="source">Source utilisée pour la mise à jour</param>
        /// <param name="existingItem">Item existant à mettre à jour</param>
        /// <exception cref="ArgumentException"></exception>
        protected override bool UpdateTResult(Record source, Node existingItem)
        {
            //TODO: Ne pas mettre hasChanged par défaut
            //et faire la détection des changements (long à programmer)
            bool hasChanged = false;

            // Validations
            if (source == null)
                throw new ArgumentException("La source ne doit pas être vide", "source");
            if (existingItem == null)
                throw new ArgumentException("L'item à mettre à jour ne doit pas être vide", "existingItem");
            if (!source.NodeTypeId.HasValue)
                throw new Exception("Please ResolveTemporyData.");

            if (existingItem.IsDeleted)
            {
                AddLog(String.Format("L'élément ({0}/{1}) a été restauré.",
                    existingItem.Name, existingItem.ID));
                existingItem.IsDeleted = false;
                hasChanged = true;
            }

            if (existingItem.Position != source.Position)
            {
                AddLog(String.Format("L'élément ({0}/{1}) a changé de position.",
                    existingItem.Name, existingItem.ID));
                existingItem.Position = source.Position;
                hasChanged = true;
            }

            if (existingItem.ParentID != source.ParentId)
            {
                AddLog(String.Format("L'élément ({0}/{1}) a été déplacé.",
                    existingItem.Name, existingItem.ID));
                existingItem.ParentID = source.ParentId;
                hasChanged = true;
            }

            var addedCultures = AddInstances(source, existingItem);

            addedCultures.ForEach(c => AddLog(String.Format("Une instance ({0}) a été ajoutée à l'élément ({1}/{2}).",
                c, source.Name, source.Id)));

            if (addedCultures.Count > 0)
                hasChanged = true;

            // Mettre à jour chacune des propriétés
            foreach (var property in source.Properties)
            {
                // Aller chercher la propriété demandée pour ce noeud
                NodeProperty nodeProperty;
                var culture = "neutral";

                if (property.CultureName == "")
                    nodeProperty = existingItem.DefaultInstance.Properties
                        .FirstOrDefault(p => p.Name
                            .Equals(property.NodeTypePropertyName, StringComparison.OrdinalIgnoreCase));
                else
                {
                    nodeProperty = existingItem[property.CultureName].Properties
                        .FirstOrDefault(p => p.Name
                            .Equals(property.NodeTypePropertyName, StringComparison.OrdinalIgnoreCase));
                    culture = property.CultureName;
                }

                if (nodeProperty == null)
                {
                    if (Options.UnknownPropertiesAsException)
                        AddException("No property " + property.NodeTypePropertyName +
                            " on the type " + existingItem.NodeType.Name + " for culture " + culture + ".");
                }
                else
                {
                    // Mettre la propriété à jour
                    if (property.PropertyType == PropertyType.ManyToMany)
                    {
                        var tempValue2 = new List<Guid>();
                        var tempValue = property.Value as IEnumerable<Guid>;

                        if (tempValue == null)
                        {
                            var tempValue3 = property.Value as IEnumerable<object>;

                            if (tempValue3 != null)
                            {
                                tempValue2.AddRange(tempValue3.Select(guid => new Guid(guid.ToString())));
                            }
                        }
                        else
                        {
                            tempValue2 = tempValue.ToList();
                        }

                        var manyToManyNodeProperty = nodeProperty as ManyToManyNodeProperty;

                        if (!manyToManyNodeProperty.Values.SequenceEqual<Guid>(tempValue2))
                        {
                            hasChanged = true;
                            AddLog(String.Format("La valeur de la propriété {0} de l'élément ({1}/{2}) à été modifiée pour la culture : {3}.",
                                property.NodeTypePropertyName, source.Name, source.Id, String.IsNullOrEmpty(property.CultureName) ? "neutre" : property.CultureName));
                            manyToManyNodeProperty.SetValue(tempValue2);
                        }
                    }

                    else
                    {
                        if (nodeProperty.Value == null || property.Value == null)
                        {
                            if (nodeProperty.Value != null || property.Value != null)
                            {
                                hasChanged = true;
                                AddLog(String.Format("La valeur de la propriété {0} de l'élément ({1}/{2}) à été modifiée pour la culture : {3}.",
                                    property.NodeTypePropertyName, source.Name, source.Id, String.IsNullOrEmpty(property.CultureName) ? "neutre" : property.CultureName));
                                nodeProperty.SetValue(property.Value);
                            }
                        }
                        else if (String.Compare(nodeProperty.Value.ToString(),
                            property.Value.ToString()) != 0)
                        {
                            hasChanged = true;
                            AddLog(String.Format("La valeur de la propriété {0} de l'élément ({1}/{2}) à été modifiée pour la culture : {3}.",
                                property.NodeTypePropertyName, source.Name, source.Id, String.IsNullOrEmpty(property.CultureName) ? "neutre" : property.CultureName));
                            nodeProperty.SetValue(property.Value);
                        }
                    }
                }
            }

            if (source.IsActive.HasValue && existingItem.DefaultInstance.IsActive != source.IsActive.Value)
            {
                existingItem.DefaultInstance.IsActive = source.IsActive.Value;

                if (source.IsActive.Value)
                    AddLog(String.Format("L'élément ({0}/{1}) a été activé.",
                        existingItem.Name, existingItem.ID));
                else
                    AddLog(String.Format("L'élément ({0}/{1}) a été désactivé.",
                        existingItem.Name, existingItem.ID));
            }


            //TODO: Affecter la date de l'instance plutôt que celle du noeud.
            if (source.CreatedOn.HasValue && existingItem.CreatedOn != source.CreatedOn.Value)
            {
                existingItem.CreatedOn = source.CreatedOn.Value;

                AddLog(String.Format("La date de création de d'élément ({0}/{1}) a été modifiée.",
                    existingItem.Name, existingItem.ID));
            }

            //TODO: Affecter la date de l'instance plutôt que celle du noeud.
            if (source.UpdatedOn.HasValue && existingItem.EditedOn != source.UpdatedOn)
            {
                existingItem.EditedOn = source.UpdatedOn;

                AddLog(String.Format("La date de mise à jour de d'élément ({0}/{1}) a été modifiée.",
                    existingItem.Name, existingItem.ID));
            }

            //TODO: Affecter le CreatedBy et UpdatedBy

            return hasChanged;
        }

        protected override Node NewTResult(Record record)
        {
            if (!record.NodeTypeId.HasValue)
                throw new Exception("Please ResolveTemporyData.");

            if (!record.Id.HasValue)
                record.Id = Guid.NewGuid();

            var node = new Node(record.Id.Value, record.NodeTypeId.Value, record.ParentId, null, null, false)
                           {
                               Position = record.Position,
                               Status = EntityStates.New,
                               IsSubEntity = record.IsSubEntity,
                               CreatedBy = record.CreatedBy,
                               CreatedOn = record.CreatedOn ?? DateTime.Now
                           };

            AddInstances(record, node);

            foreach (var property in record.Properties)
            {


                NodeProperty nodeProperty;
                var culture = "neutral";

                if (property.CultureName == "")
                    nodeProperty = node.DefaultInstance.Properties
                        .FirstOrDefault(p => p.Name.Equals(property.NodeTypePropertyName,
                                                         StringComparison.OrdinalIgnoreCase));
                else
                {
                    nodeProperty = node[property.CultureName].Properties
                        .FirstOrDefault(p => p.Name.Equals(property.NodeTypePropertyName,
                                                         StringComparison.OrdinalIgnoreCase));
                    culture = property.CultureName;
                }

                if (nodeProperty == null)
                {
                    if (Options.UnknownPropertiesAsException)
                        AddException("No property " + property.NodeTypePropertyName +
                            " on the type " + node.NodeType.Name + " for culture " + culture + ".");
                }
                else
                {
                    // Mettre la propriété à jour
                    var value = property.Value;

                    if (property.PropertyType == PropertyType.ManyToMany)
                    {
                        var tempValue2 = new List<Guid>();
                        var tempValue = property.Value as IEnumerable<Guid>;

                        if (tempValue == null)
                        {
                            var tempValue3 = property.Value as IEnumerable<object>;

                            if (tempValue3 != null)
                            {
                                tempValue2.AddRange(tempValue3.Select(guid => new Guid(guid.ToString())));
                            }
                        }
                        else
                        {
                            tempValue2 = tempValue.ToList();
                        }

                        value = tempValue2;
                    }

                    nodeProperty.SetValue(value);
                }
            }

            if (record.IsActive.HasValue && record.IsActive.Value == false)
                node.DefaultInstance.IsActive = false;

            //TODO: Affecter la date de l'instance plutôt que celle du noeud.
            if (record.CreatedOn.HasValue)
                node.CreatedOn = record.CreatedOn.Value;

            //TODO: Affecter la date de l'instance plutôt que celle du noeud.
            if (record.UpdatedOn.HasValue)
            {
                node.EditedOn = record.UpdatedOn.Value;
                node.BlockDirty = true;
            }

            AddLog(String.Format("L'élément ({0}/{1}) a été ajouté.", record.Name, record.Id));

            return node;
        }

        private HashSet<string> AddInstances(Record record, Node node)
        {
            var addedInstances = new HashSet<string>();

            if (node.NodeType.IsLanguageNeutral)
            {
                if (!node.InternalInstances.Any(i => i.Culture.Name.EqualsIgnoreCase("")))
                {
                    addedInstances.Add("");
                    node.AddNodeInstance("");

                    node.InternalInstances[""].IsActive = !record.InactiveCultureNames.Any(c => c.Equals(""));
                }
            }
            else
            {
                var cultures = record.Properties.Select(p => p.CultureName)
                    .Where(c => c != "").Distinct().ToList();

                if (cultures.Count == 0)
                    cultures.Add(record.DefaultCultureName);

                cultures.Where(c => !node.InternalInstances
                    .Select(i => i.Culture.Name.ToLower())
                    .Contains(c.ToLower()))
                    .ForEach(c =>
                         {
                             addedInstances.Add(c);
                             node.AddNodeInstance(c);
                             node.InternalInstances[c].IsActive = !record.InactiveCultureNames
                                .Any(c2 => c2.Equals(c));
                         });
            }

            return addedInstances;
        }

        protected override bool ResolveTemporaryData(Record item)
        {
            var exceptionCount = Exceptions.Count();

            NodeType type = null;

            if (!item.NodeTypeId.HasValue)
            {
                type = NodeTypeManager.GetNodeType(item.NodeTypeName);

                if (type == null)
                    AddException(String.Concat("The type ", item.NodeTypeName, " does not exist."));
                else
                    item.NodeTypeId = type.ID;
            }

            SetSynchronizeAction(item);

            if (item.NodeTypeId.HasValue)
            {
                if (!String.IsNullOrEmpty(item.TemporaryParentId))
                {
                    var tmp = SourceItems.FirstOrDefault(i => (string.IsNullOrEmpty(item.ParentIdReferencedTypeName) || i.NodeTypeName == item.ParentIdReferencedTypeName) && i.TemporaryId != null &&
                        i.TemporaryId.Equals(item.TemporaryParentId,
                        StringComparison.OrdinalIgnoreCase));

                    item.ParentId = tmp == null ? null : tmp.Id;

                    if (item.ParentId == null)
                    {
                        item.ParentId = ResolveParentId(item.TemporaryParentId, item.ParentIdPropertyName,
                                                       item.ParentIdReferencedTypeName);
                    }

                    if (item.ParentId == null)
                        AddException("The ParentNodeId could not be resolved for TemporaryParentNodeId: " + item.TemporaryParentId);
                }

                item.Properties.Where(p => String.IsNullOrEmpty(p.NodeTypePropertyName))
                    .ForEach(p => ResolveTemporaryData(p, type, item));
            }

            return Exceptions.Count() <= exceptionCount;
        }

        protected override bool ResolveLookups(Record item)
        {
            var exceptionCount = Exceptions.Count();

            item.Properties.Where(p => p.Value != null && !String.IsNullOrEmpty(p.ReferencedTypeName))
                    .ForEach(p => ResolveLookup(p, item, Options.UnresolvedLookupsAsException));

            return Exceptions.Count() <= exceptionCount;
        }

        private Guid? ResolveParentId(string parentId, string propertyName, string parentTypeName)
        {
            Guid? result = null;

            var type = NodeTypeManager.GetNodeType(parentTypeName);
            var prop = type.GetAllRefreshedProperties().Where(p => p.Name
                        .Equals(propertyName, StringComparison.OrdinalIgnoreCase)).First();

            var lookupQueryResult = NodeManager.MakeGetNodeByTypeQuery(type.ID, false)
                            .Where(prop.ID, Comparison.Equal, parentId)
                            .Take(1)
                            .AsQuery<NodeWrapper>().Execute().FirstOrDefault();

            if (lookupQueryResult != null && lookupQueryResult.InnerNode != null)
                result = lookupQueryResult.InnerNode.ID;

            return result;
        }

        protected void ResolveLookup(RecordProperty property, Record record, bool unresolvedLookupAsException)
        {
            if (record.IgnoreImport || property.Value == null || property.Value.ToString().Length == 0) return;

            var allValues = new HashSet<string>();

            if (property.PropertyType == PropertyType.ManyToMany)
            {
                property.Value.ToString().Split(',').ForEach(v => allValues.Add(v));
            }
            else
            {
                allValues.Add(property.Value.ToString());
            }

            if (String.IsNullOrEmpty(property.ReferencedPropertyName))
            {
                property.Value = GetResolvedLookupIDsFromSourceItemsOrDBByNodeId(GetNodeIDsFromPropertyValues(allValues), 
                        property, record, unresolvedLookupAsException) ??
                    GetResolvedLookupIDsFromSourceItemsOrDBByTemporaryId(allValues,
                        property, record, unresolvedLookupAsException);
            }
            else
            {
                property.Value = GetResolvedLookupIDsFromSourceItemsOrDBByReferencedPropertyName(allValues,
                    property, record, unresolvedLookupAsException);
            }
        }

        private object GetResolvedLookupIDsFromSourceItemsOrDBByTemporaryId(HashSet<string> values, RecordProperty property, Record record, bool unresolvedLookupAsException)
        {
            var nodeIDs = new HashSet<Guid>();
            var type = NodeTypeManager.GetNodeType(property.ReferencedTypeName);

            foreach (var value in values)
            {
                var sourceItem = SourceItems.FirstOrDefault(i => i.NodeTypeId == type.ID && i.TemporaryId == value);

                if (sourceItem == null || !sourceItem.Id.HasValue)
                {
                    LogUnresolvedLookup(value, property, record, unresolvedLookupAsException);
                }
                else
                {
                    nodeIDs.Add(sourceItem.Id.Value);
                }
            }

            if (nodeIDs.Count == 1 && property.PropertyType != PropertyType.ManyToMany)
                return nodeIDs.First();

            return (nodeIDs.Count != values.Count) && unresolvedLookupAsException ? null : nodeIDs.ToList();
        }

        private object GetResolvedLookupIDsFromSourceItemsOrDBByReferencedPropertyName(HashSet<string> values, 
            RecordProperty property, Record record, bool unresolvedLookupAsException)
        {
            var nodeIDs = new HashSet<Guid>();
            var type = NodeTypeManager.GetNodeType(property.ReferencedTypeName);
            var prop = type.GetAllRefreshedProperties().Where(p =>
                p.Name.Equals(property.ReferencedPropertyName, StringComparison.OrdinalIgnoreCase)).First();

            foreach (var value in values)
            {
                var sourceItem = SourceItems.FirstOrDefault(i => i.NodeTypeId.Equals(type.ID) && 
                    i[property.ReferencedPropertyName].Value.ToString() == value);

                if (sourceItem == null || !sourceItem.Id.HasValue)
                {
                    var node = ResolveLookupFromDatabase(value, type, prop);

                    if (node != null && node.InnerNode != null)
                    {
                        nodeIDs.Add(node.ID);
                    }
                    else
                    {
                        LogUnresolvedLookup(value, property, record, unresolvedLookupAsException);
                    }
                }
                else
                {
                    nodeIDs.Add(sourceItem.Id.Value);
                }
            }

            if (nodeIDs.Count == 1 && property.PropertyType != PropertyType.ManyToMany)
                return nodeIDs.First();

            return (nodeIDs.Count != values.Count) && unresolvedLookupAsException ? null : nodeIDs.ToList();
        }

        private object GetResolvedLookupIDsFromSourceItemsOrDBByNodeId(HashSet<Guid> nodeIDs, RecordProperty property, Record record,
            bool unresolvedLookupAsException)
        {
            var type = NodeTypeManager.GetNodeType(property.ReferencedTypeName);
            var unresolvedLookups = nodeIDs.Where(n => !SourceItems.Any(r => r.NodeTypeId == type.ID && r.Id == n)).ToList();
            var resolvedLookups = nodeIDs.Except(unresolvedLookups).ToList();

            if (unresolvedLookups.Any())
            {
                var nodes = NodeManager.GetNodes(unresolvedLookups);

                foreach (var unresolvedLookup in unresolvedLookups)
                {
                    if(nodes.Any(n => n.NodeId == unresolvedLookup))
                    {
                        resolvedLookups.Add(unresolvedLookup);
                    }
                    else
                    {
                        LogUnresolvedLookup(unresolvedLookup.ToString(), property, record, unresolvedLookupAsException);
                    }
                }
            }

            resolvedLookups = resolvedLookups.Distinct().ToList();

            if (resolvedLookups.Count == 1)
                return resolvedLookups.First();

            return (nodeIDs.Count != resolvedLookups.Count) && unresolvedLookupAsException ? null : resolvedLookups;
        }

        private void LogUnresolvedLookup(string unresolvedLookup, RecordProperty property, Record record,
            bool unresolvedLookupAsException)
        {
            var text = String.Format("Lookup could not be resolve for record with temporary id {0}, referenced type name {1}, referenced id {2}.",
                record.TemporaryId, property.ReferencedTypeName, unresolvedLookup);

            if (unresolvedLookupAsException)
            {
                AddException(text);
            }
            else
            {
                AddWarning(text);
            }
        }

        private HashSet<Guid> GetNodeIDsFromPropertyValues(IEnumerable<string> values)
        {
            var guidList = new HashSet<Guid>();

            foreach (var value in values)
            {
                Guid g;
                if (value.IsGuidInline(out g))
                {
                    guidList.Add(g);
                }
                else
                {
                    throw new Exception("todo");
                }
            }

            return guidList;
        }

        protected virtual NodeWrapper ResolveLookupFromDatabase(string value, NodeType type, NodeTypeProperty prop)
        {
            return NodeManager.MakeGetNodeByTypeQuery(type.ID, false)
                .Where(prop.ID, Comparison.Equal, value)
                .Take(1)
                .AsQuery<NodeWrapper>().Execute().FirstOrDefault();
        }

        protected void ResolveTemporaryData(RecordProperty property, NodeType type, Record record)
        {
            if (!record.IgnoreImport)
            {
                var tempType = type ?? NodeTypeManager.GetNodeType(record.NodeTypeId.Value);

                if (tempType == null)
                    AddException(String.Format("Le type ({0}) référencé par le noeud ({1}/{2}) n'existe pas.",
                        record.NodeTypeId.Value, record.Id, record.Name));
                else
                {
                    var typeProperty = tempType.GetAllProperties()
                        .FirstOrDefault(p => p
                       .ID.Equals(property.NodeTypePropertyId));

                    if (typeProperty == null)
                        AddException(String.Format("La propriété {0} référencée par le noeud {1} n'est pas présente sur le type {2}.",
                            property.NodeTypePropertyId, record.Id, tempType.Name));

                    property.NodeTypePropertyName = typeProperty.Name;
                }
            }
        }
    }
}
